﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace AstoriaPlus.Extensions
{
	internal static class QueryEvaluator<TEntity, TContext>
	{
		public static Expression<Func<TEntity, bool>> Eval(Expression<Func<TEntity, TContext, bool>> expression, TContext context)
		{
			//Console.WriteLine("Before: "+ expression);
			var evaluatedExpression = new QueryExpressionVisitor(context).Visit(expression);
			var result = (Expression<Func<TEntity, bool>>)RemoveContextParameter((LambdaExpression)evaluatedExpression);

			//Console.WriteLine("After:  " + result);
			return result;
		}

		private static LambdaExpression RemoveContextParameter(LambdaExpression expression)
		{
			var result = Expression.Lambda<Func<TEntity, bool>>(
				expression.Body, expression.Parameters.First());

			return result;
		}

		class QueryExpressionVisitor : ExpressionVisitor
		{
			private TContext context;

			public QueryExpressionVisitor(TContext context)
			{
				this.context = context;
			}

			public override Expression Visit(Expression node)
			{
				if (node is BinaryExpression
					|| node is ParameterExpression
					|| node is LambdaExpression
					|| node is InvocationExpression)
				{
					return base.Visit(node);
				}

				if (new IsEntityVisitor().IsEntity(node))
					return node;

				var contextParam = Expression.Parameter(typeof(TContext), "c");
				var lambda = Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(TContext), node.Type),
					RewriteContextParameter(node, contextParam), contextParam);

				var value = lambda.Compile().DynamicInvoke(this.context);

				return Expression.Constant(value, node.Type);
			}

			private Expression RewriteContextParameter(Expression node, ParameterExpression contextParam)
			{
				return new ParameterRewriterVisitor(contextParam).Visit(node);
			}
		}

		class ParameterRewriterVisitor : ExpressionVisitor
		{
			private ParameterExpression contextParam;

			public ParameterRewriterVisitor(ParameterExpression contextParam)
			{
				this.contextParam = contextParam;
			}

			protected override Expression VisitParameter(ParameterExpression node)
			{
				if (node.Type == this.contextParam.Type)
					return this.contextParam;

				return node;
			}
		}

		class IsEntityVisitor : ExpressionVisitor
		{
			public bool IsEntityExpression { get; set; }

			public bool IsEntity(Expression expression)
			{
				this.Visit(expression);

				return this.IsEntityExpression;
			}

			protected override Expression VisitParameter(ParameterExpression node)
			{
				if (node.Type == typeof(TEntity))
					this.IsEntityExpression = true;

				return base.VisitParameter(node);
			}
		}
	}

	//public class Foo
	//{
	//    public int Id { get; set; }
	//    public Bar Bar { get; set; }
	//}

	//public class Bar
	//{
	//    public int Id { get; set; }
	//}

	//public class Context
	//{
	//    public int UserId { get; set; }
	//}

	//public static class HttpContext
	//{
	//    public static User CurrentUser { get; set; }
	//}

	//public class User
	//{
	//    public bool IsAuthenticated { get; set; }
	//}

	//public class Test
	//{
	//    public void Query()
	//    {
	//        HttpContext.CurrentUser = new User { IsAuthenticated = false };
	//        Expression<Func<Foo, Context, bool>> query1 =
	//            (e, cont) => e.Id == cont.UserId && (HttpContext.CurrentUser.IsAuthenticated || e.Bar.Id == 7);

	//        Expression<Func<Foo, Context, bool>> query2 =
	//            (e, cont) => e.Bar == null;

	//        Expression<Func<Foo, Context, bool>> combined = ((QueryExpression<Foo, Context>)query1) | (!((QueryExpression<Foo, Context>)query2));

	//        var eval = QueryEvaluator<Foo, Context>.Eval(combined, new Context { UserId = 1 });
	//        Console.WriteLine(eval);

	//        Console.WriteLine(eval.Compile().Invoke(new Foo { Id = 1, Bar = new Bar { Id = 7 } }));
	//        Console.WriteLine(eval.Compile().Invoke(new Foo { Id = 1, Bar = new Bar { Id = 6 } }));
	//        //Console.WriteLine(eval.Compile().Invoke(new Foo { Id = 1, Bar = null }));
	//    }
	//}
}
